Skip to content

perf(api): pass ?collapse=organization on project detail fetches#818

Merged
BYK merged 2 commits intomainfrom
byk/perf-collapse-organization-project-detail
Apr 22, 2026
Merged

perf(api): pass ?collapse=organization on project detail fetches#818
BYK merged 2 commits intomainfrom
byk/perf-collapse-organization-project-detail

Conversation

@BYK
Copy link
Copy Markdown
Member

@BYK BYK commented Apr 22, 2026

Summary

Cuts ~400-500ms off GET /api/0/projects/{org}/{project}/ by asking the server to skip full-org serialization. The collapsed response carries only {id, slug} for organization instead of the full payload (feature flags, options, etc).

Colleague flagged this optimization noting the UI already benefits — every CLI hot path that fetches project details can ride the same win.

Hot paths that benefit

  • DSN auto-detect (numeric orgId/projectId → slug resolution)
  • Explicit <org>/<project> target validation in resolveTarget
  • findProjectsBySlug (one getProject per accessible org, multiplying the saving)
  • project view, project delete, dashboard create, release list, trace view, init, and others

All project detail fetches in the CLI funnel through getProject() in src/lib/api/projects.ts, so a single change covers everything.

Handling the trimmed response

The collapsed payload drops organization.name. Four call sites displayed it:

  • src/lib/resolve-target.ts — three DSN resolution paths + one cache-seed after explicit target validation
  • src/lib/dsn/resolver.ts — DSN → ResolvedProject
  • src/lib/formatters/human.tsproject view's Organization: row

All now use a new resolveOrgDisplayName(orgSlug, explicitName?) helper with the fallback chain:

  1. Explicit name if the server returned it (self-hosted / older Sentry ignore collapse and return the full payload — handled transparently)
  2. getCachedOrganizations() lookup — populated on login and every org-fanout operation
  3. Slug itself as last resort

The cosmetic "name → slug" degradation only surfaces on cold-cache + collapse-honoring server, which is vanishingly rare (every auth flow warms the cache).

JSON output shape stays stable

sentry project view --json previously returned the full organization object (including name, feature flags, options). Since getProject() now collapses the server response, project view gains a jsonTransform that re-hydrates organization.name via resolveOrgDisplayName() before serialization. Downstream agents and scripts that scrape .organization.name continue to see a value.

When the server returns name (self-hosted / ignores collapse), the transform preserves it as-is. When absent, it falls back through the cached org list to the slug. Feature flags and options are no longer returned — this is a deliberate tradeoff, as those fields aren't documented stable CLI output and dropping them is where the perf win comes from.

Response cache invalidation is now prefix-based

The response cache keys by full URL (including sorted query params via buildCacheKey/normalizeUrl). Pre-PR, invalidateCachedResponse(baseUrl) would match the cached entry exactly. Post-PR, cached entries live under baseUrl?collapse=organization, so an exact-match invalidation would silently miss — project deleteproject view would serve pre-deletion data.

invalidateProjectCaches switches to invalidateCachedResponsesMatching (prefix sweep) to catch the collapsed variant plus any future query-param additions. The trailing slash on buildApiUrl output guarantees the prefix can't collide with sibling projects (projects/acme/frontend/ does not prefix-match projects/acme/frontend-old/).

Side effect: sub-resources like /projects/acme/frontend/keys/ and /projects/acme/frontend/trace-items/... also get swept on project delete. This is desirable — those children would 404 after the parent is gone anyway.

Tests

Added to test/lib/api-client.test.ts:

  • getProject sends ?collapse=organization query parameter — asserts the outgoing URL carries the query param.
  • getProject tolerates collapsed response missing organization.name — confirms no blow-up when name is absent.
  • 4 resolveOrgDisplayName cases: explicit name wins / cache fallback / slug fallback / empty-string falsy handling.

Added to test/lib/formatters/human.details.test.ts:

  • falls back to org slug when collapsed response omits name — module-level useTestConfigDir ensures an empty org_regions table, regex-match asserts the slug appears in both the display-name and parens positions (stray cached name would fail).

Added to test/commands/project/view.func.test.ts:

  • JSON output re-hydrates organization.name when API response omits it — exercises the new jsonTransform slug-fallback path.
  • JSON output preserves organization.name when API response includes it — self-hosted / older-Sentry compat.
  • JSON output still strips detectedFrom (human-only field) — regression guard for the jsonExcludejsonTransform switch.
  • JSON output honours --fields filter — confirms field filtering still works on top of the transform.

Verification

  • bun run typecheck
  • bun run lint ✓ (only a pre-existing unrelated warning)
  • bun run test:unit → 5676 pass / 0 fail (+4 new from follow-up)
  • bun run test:isolated → 138 pass / 0 fail

Follow-ups (out of scope)

  • Team detail (retrieveATeam) has the same ?collapse=organization support per the SDK types — same perf win available for team list etc.
  • Similar-shape audit on other endpoints returning nested org payloads.

Cuts ~400-500ms off the `GET /api/0/projects/{org}/{project}/` endpoint
by asking the server to skip full-org serialization. The collapsed
response carries only `{id, slug}` for `organization` instead of the
full payload (feature flags, options, etc).

Hot paths that benefit:

- DSN auto-detect (numeric orgId/projectId to slug resolution)
- Explicit `<org>/<project>` target validation
- `findProjectsBySlug` (one call per accessible org)
- `project view`, `project delete`, `dashboard create`,
  `release list`, `trace view`, `init`, and others

Since the collapsed payload drops `organization.name`, callers that
displayed it now use a new `resolveOrgDisplayName()` helper with the
fallback chain: explicit name -> cached org list -> slug. The org-list
cache is populated on login and every org-fanout operation, so the
cosmetic "name -> slug" degradation is vanishingly rare in practice.

Self-hosted and older Sentry versions ignore unknown query params and
return the full `organization`, so the fallback handles both cases
uniformly.

`invalidateProjectCaches` switches to `invalidateCachedResponsesMatching`
prefix-sweep because the response cache keys include query strings — a
prefix sweep catches the collapsed variant plus any future query-param
additions.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 22, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://cli.sentry.dev/_preview/pr-818/

Built to branch gh-pages at 2026-04-22 13:50 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 22, 2026

Codecov Results 📊

138 passed | Total: 138 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests
Passed Tests
Failed Tests
Skipped Tests

✨ No test changes detected

All tests are passing successfully.

✅ Patch coverage is 82.19%. Project has 1971 uncovered lines.
❌ Project coverage is 95.16%. Comparing base (base) to head (head).

Files with missing lines (2)
File Patch % Lines
src/lib/resolve-target.ts 45.45% ⚠️ 12 Missing
src/lib/api/projects.ts 92.86% ⚠️ 1 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
- Coverage    95.20%    95.16%    -0.04%
==========================================
  Files          282       282         —
  Lines        40676     40721       +45
  Branches         0         0         —
==========================================
+ Hits         38725     38750       +25
- Misses        1951      1971       +20
- Partials         0         0         —

Generated by Codecov Action

Review follow-ups for #818:

1. `sentry project view --json` was silently losing `organization.name`
   after the `?collapse=organization` switch, breaking agents/scripts
   that scrape `.organization.name`. Added a `jsonTransform` that
   re-hydrates `organization.name` via `resolveOrgDisplayName()` before
   serialization, so the JSON shape stays stable across CLI versions.
   When the server returns `name` (self-hosted / ignores `collapse`),
   the transform preserves it. When absent, it falls back through the
   cached org list to the slug.

2. The `falls back to org slug when collapsed response omits name` test
   in `human.details.test.ts` had two weaknesses flagged in review:
   (a) no `useTestConfigDir` isolation — an earlier test in the same
   process could seed `org_regions` and mask the slug-fallback path;
   (b) `expect(result).toContain("acme")` would pass regardless of
   what `resolveOrgDisplayName` returned because the slug always
   appears inside the parens. Added module-level `useTestConfigDir`
   and tightened the assertion to a regex that requires the slug in
   BOTH the display-name and the parens position.

Added 4 new tests to `project/view.func.test.ts` covering: JSON
hydration when `organization.name` is absent, preservation when it's
present, continued `detectedFrom` stripping, and `--fields` filter
interaction.
@BYK BYK merged commit ba7fc85 into main Apr 22, 2026
26 checks passed
@BYK BYK deleted the byk/perf-collapse-organization-project-detail branch April 22, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant